package com.sec.android.app.myfiles.module.abstraction;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteFullException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import android.widget.Toast;

import com.sec.android.app.myfiles.R;
import com.sec.android.app.myfiles.info.AppConstants;
import com.sec.android.app.myfiles.log.Log;
import com.sec.android.app.myfiles.module.abstraction.FileRecord.StorageType;
import com.sec.android.app.myfiles.provider.DbTableInfo;
import com.sec.android.app.myfiles.provider.DbTableInfo.COLUMN_ID;
import com.sec.android.app.myfiles.util.chain.Chain;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * <pre>
 * Abstract class of provider implement
 * It works based on chain responsibility pattern
 * </pre>
 *
 * @author jae.bae
 */
public abstract class AbsProviderImp implements Chain<AbsProviderImp> {

    AbsProviderImp mNext; // for chain of responsibility pattern
    protected SQLiteDatabase mDB;
    protected Context mContext;

    protected DbTableInfo mDbTableInfo;

    protected static final int MIN_MYFILE_2016_DB_VERSION = 200;

    public AbsProviderImp(Context context) {
        mContext = context;
        mDbTableInfo = DbTableInfo.getInstance(getStorage());
    }

    public final void setDB(SQLiteDatabase db) {
        mDB = db;
        onSetDB();

        if (mNext != null) {
            mNext.setDB(db);
        }
    }

    protected void onSetDB() {
    }

    /**
     * set Next chain item
     *
     * @param next
     * @return
     */
    @Override
    public void setNext(AbsProviderImp next) {
        mNext = next;
    }

    public final DbTableInfo getDbTableInfo() {
        return mDbTableInfo;
    }

    /**
     * get URI of provider
     *
     * @return
     */
    protected final String getMyUri() {
        return mDbTableInfo.getUri();
    }

    /**
     * check this provider can handle uri
     *
     * @param uri
     * @return boolean
     */
    public final boolean isMyUri(Uri uri) {
        return uri.toString().startsWith(getMyUri());
    }

    /**
     * Update DB table
     *
     * @param db
     * @param oldVersion
     * @param newVersion
     */
    public final void updateDB(SQLiteDatabase db, int oldVersion, int newVersion) {
        _updateDB(db, oldVersion, newVersion);

        if (mNext != null) {
            mNext.updateDB(db, oldVersion, newVersion);
        }
    }

    public final void clearDB(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (needClearDB()) {
            _updateDB(db, oldVersion, newVersion);
        }

        if (mNext != null) {
            mNext.clearDB(db, oldVersion, newVersion);
        }
    }

    /**
     * get query for create table
     *
     * @return SQL query for create table
     */
    public final String getCreateTableQuery() {
        StringBuilder buf = new StringBuilder();
        buf.append("CREATE TABLE IF NOT EXISTS ").append(getTableName()).append(" (");

        for (int i = 0; i < mDbTableInfo.getColumnCount(); i++) {
            COLUMN_ID id = mDbTableInfo.getColumnID(i);

            buf.append(mDbTableInfo.getColumnName(id)).append(" ").append(mDbTableInfo.getColumnAttr(id));
            if (i < (mDbTableInfo.getColumnCount() - 1)) {
                buf.append(", ");
            }
        }

        buf.append(");");

        return buf.toString();
    }

    /**
     * get query for drop table
     *
     * @return SQL query for drop table
     */
    public final String getDropTableQuery() {
        return "DROP TABLE IF EXISTS " + getTableName();
    }

    public String[] getCreateTriggerQuery() {
        return null;
    }

    public String[] getDropTriggerQuery() {
        return null;
    }

    protected void _updateDB(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (mDbTableInfo.isRealDb()) {
            updateTable(db, oldVersion, newVersion);
            updateTrigger(db, oldVersion, newVersion);
        }
    }

    protected void updateTable(SQLiteDatabase db, int oldVersion, int newVersion) {
        try {
            db.execSQL(getDropTableQuery());
            db.execSQL(getCreateTableQuery());
        } catch (SQLiteFullException e) {
            Log.e(this, "SQLiteFullException:" + e.toString());
        }
    }

    protected void updateTrigger(SQLiteDatabase db, int oldVersion, int newVersion) {
        try {
            String[] queries = getDropTriggerQuery();
            if (queries != null) {
                for (String query : queries) {
                    if (!TextUtils.isEmpty(query)) {
                        db.execSQL(query);
                    }
                }
            }
            queries = getCreateTriggerQuery();
            if (queries != null) {
                for (String query : queries) {
                    if (!TextUtils.isEmpty(query)) {
                        db.execSQL(query);
                    }
                }
            }
        } catch (SQLiteFullException e) {
            Log.e(this, "SQLiteFullException:" + e.toString());
        }
    }

    public final void clearCache(Uri uri, FileRecord record) {
        if (uri != null) {
            if (isMyUri(uri)) {
                _clearCache(record);
            } else {
                if (mNext != null) {
                    mNext.clearCache(uri, record);
                }
            }
        } else {
            _clearCache(record); //do all chain when uri is null
            if (mNext != null) {
                mNext.clearCache(uri, record);
            }
        }
    }

    public final void stopRefreshDb(Uri uri) {
        if (uri != null) {
            if (isMyUri(uri)) {
                _stopRefreshDb();
            } else {
                if (mNext != null) {
                    mNext.stopRefreshDb(uri);
                }
            }
        } else {
            _stopRefreshDb();
            if (mNext != null) {
                mNext.stopRefreshDb(uri);
            }
        }
    }

    /**
     * check curRecord folder need to refresh and refresh DB
     *
     * @param uri
     * @param curRecord
     */
    public final void refreshDB(final Uri uri, final FileRecord curRecord, boolean isForced) {
        if (isMyUri(uri)) {
            if (isForced || needRefresh(uri, curRecord)) {
                _refreshDB(uri, curRecord);
            }
        } else {
            if (mNext != null) {
                mNext.refreshDB(uri, curRecord, isForced);
            }
        }
    }

    /**
     * <pre>
     * Query method
     *
     * note : Do not override this method, but override
     *        {@link #_query(Uri, String[], String, String[], String)}
     * </pre>
     *
     * @param uri
     * @param projectionIn
     * @param selection
     * @param selectionArgs
     * @param sortOrder
     * @return
     */
    public final Cursor query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sortOrder) {
        Cursor ret = null;
        if (isMyUri(uri)) {
            ret = _query(uri, projectionIn, selection, selectionArgs, sortOrder);

            if (ret != null) {
                ret.setNotificationUri(mContext.getContentResolver(), uri);
            }
        } else {
            if (mNext != null) {
                ret = mNext.query(uri, projectionIn, selection, selectionArgs, sortOrder);
            }
        }

        return ret;
    }

    /**
     * <pre>
     * Insert method
     *
     * note : Do not override this method, but override
     *        {@link #_insert(Uri, ContentValues)}
     *
     * @param uri
     * @param values
     * @return
     */
    public final Uri insert(Uri uri, ContentValues values) {
        Uri ret = null;
        if (isMyUri(uri)) {
            ret = _insert(uri, values);

            if (ret != null) {
                notifyChange(uri);
            }
        } else {
            if (mNext != null) {
                ret = mNext.insert(uri, values);
            }
        }
        return ret;
    }

    /**
     * <pre>
     * bulkInsert method
     *
     * note : Do not override this method, but override
     *        {@link #bulkInsert(Uri, ContentValues[])}
     * </pre>
     *
     * @param uri
     * @param values
     * @return
     */
    public final int bulkInsert(Uri uri, ContentValues[] values) {
        int ret = 0;
        if (isMyUri(uri)) {
            ret = _bulkInsert(uri, values);
            Log.d(this, "bulkInsert ret = " + ret);
            if (ret > 0) {
                notifyChange(uri);
            }
        } else {
            if (mNext != null) {
                ret = mNext.bulkInsert(uri, values);
            }
        }
        return ret;
    }

    /**
     * <pre>
     * update method
     *
     * note : Do not override this method, but override
     *        {@link #_query(Uri, String[], String, String[], String)}
     * </pre>
     *
     * @param uri
     * @param values
     * @param selection
     * @param selectionArgs
     * @return
     */
    public final int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int ret = 0;
        if (isMyUri(uri)) {
            ret = _update(values, selection, selectionArgs);
            if (ret > 0) {
                notifyChange(uri);
            }
        } else {
            if (mNext != null) {
                ret = mNext.update(uri, values, selection, selectionArgs);
            }
        }
        return ret;
    }

    /**
     * <pre>
     * delete method
     * note : Do not override this method, but override
     *         {@link #_delete(Uri, String, String[])}
     * </pre>
     *
     * @param uri
     * @param selection
     * @param selectionArgs
     * @return
     */
    public final int delete(Uri uri, String selection, String[] selectionArgs) {
        int ret = 0;
        if (isMyUri(uri)) {
            ret = _delete(selection, selectionArgs);
            if (ret > 0) {
                boolean notifyChange = true;
                Set<String> queryParametersName = uri.getQueryParameterNames();
                if (queryParametersName.contains(DbTableInfo.QUERY_PARAM_NOTIFY_CHANGE)) {
                    String paramNotifyChange = uri.getQueryParameter(DbTableInfo.QUERY_PARAM_NOTIFY_CHANGE);
                    notifyChange = Boolean.parseBoolean(paramNotifyChange);
                }
                if (notifyChange) {
                    notifyChange(uri);
                }
            }
        } else {
            if (mNext != null) {
                ret = mNext.delete(uri, selection, selectionArgs);
            }
        }
        return ret;
    }

    protected void notifyChange(Uri uri) {
        mContext.getContentResolver().notifyChange(uri, null);
    }

    protected int _bulkInsert(Uri uri, ContentValues[] values) {
        int nRet = 0;
        if (values != null && values.length > 0) {
            mDB.beginTransaction();
            try {
                for (ContentValues value : values) {
                    if (value != null) {
                        if (mDB.insert(getTableName(), null, value) >= 0) {
                            nRet++;
                        }
                    }
                }
                mDB.setTransactionSuccessful();
            } finally {
                mDB.endTransaction();
            }
        }
        return nRet;
    }

    protected Uri _insert(Uri uri, ContentValues values) {
        Uri ret = null;
        long rowId = mDB.insert(getTableName(), null, values);

        if (rowId > 0) {
            ret = ContentUris.withAppendedId(Uri.parse(getMyUri()), rowId);
        }
        return ret;
    }

    protected int _delete(String selection, String[] selectionArgs) {
        return mDB.delete(getTableName(), selection, selectionArgs);
    }

    protected int _update(ContentValues values, String selection, String[] selectionArgs) {
        try {
            return mDB.update(getTableName(), values, selection, selectionArgs);
        } catch (IllegalArgumentException e) {
            Log.e(this, "IllegalArgumentException:" + e.toString());
        } catch (SQLiteFullException e) {
            Log.e(this, "SQLiteFullException:" + e.toString());
        }
        return 0;
    }

    protected Cursor _query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sortOrder) {
        Cursor c;
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(getTableName());
        String limitString = uri.getQueryParameter(DbTableInfo.QUERY_PARAM_LIMIT);
        if (limitString == null) {
            c = getMergedCursor(uri, projectionIn, selection, selectionArgs, sortOrder, qb);
        } else {
            c = getCursor(projectionIn, selection, selectionArgs, sortOrder, qb, limitString);
        }
        return c;
    }

    private Cursor getMergedCursor(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sortOrder, SQLiteQueryBuilder qb) {
        Cursor c = null;
        int offset = 0;
        int limit = AppConstants.MAX_CURSOR_DATA_COUNT;
        String limitString;
        List<Cursor> mergeCursorList = new ArrayList<>();
        boolean finished = false;
        try {
            do {
                limitString = offset + "," + limit;
                c = getCursor(projectionIn, selection, selectionArgs, sortOrder, qb, limitString);
                if (c == null) {
                    return null;
                }
                mergeCursorList.add(c);
                offset += limit;
            } while (c.getCount() == limit);

            if (mergeCursorList.size() > 1) {
                mergeCursorList.get(0).setNotificationUri(mContext.getContentResolver(), uri);
                c = new MergeCursor(mergeCursorList.toArray(new Cursor[mergeCursorList.size()]));
            }
            finished = true;
        } finally {
            //Close cursors if cancelled
            if (!finished) {
                for (Cursor cursor : mergeCursorList) {
                    cursor.close();
                }
            }
        }
        return c;
    }


    private Cursor getCursor(String[] projectionIn, String selection, String[] selectionArgs, String sortOrder, SQLiteQueryBuilder qb, String limitString) {
        Cursor c = null;
        try {
            c = qb.query(mDB, projectionIn, selection, selectionArgs, null, null, sortOrder, limitString);
        } catch (SQLiteFullException e) {
            Toast.makeText(mContext, R.string.not_enough_memory_popup, Toast.LENGTH_SHORT).show();
            Log.e(this, "SQLiteFullException:" + e.toString());
        } catch (SQLiteException e) {
            Log.e(this, "SQLiteException:" + e.toString());
        } catch (IllegalArgumentException e) {
            Log.e(this, "selection:" + selection);
            if (selectionArgs != null) {
                for (String arg : selectionArgs) {
                    Log.e(this, "selectionArgs - " + arg);
                }
            } else {
                Log.e(this, "selectionArgs - null");
            }
            Log.e(this, "IllegalArgumentException:" + e.toString());
        }
        return c;
    }

    protected String getTableName() {
        return getTableName(getStorage());
    }

    protected static String getTableName(StorageType storageType) {
        String tableName = "";
        DbTableInfo info = DbTableInfo.getInstance(storageType);
        if (info != null)
            tableName = info.getTableName();
        return tableName;
    }

    /**
     * get storage type of current provider
     *
     * @return
     */
    protected abstract StorageType getStorage();

    /**
     * check curRecord folder need to refresh DB
     *
     * @param uri
     * @param curRecord
     * @return true/false
     */
    protected abstract boolean needRefresh(Uri uri, FileRecord curRecord);

    /**
     * refresh DB of curRecord folder
     *
     * @param uri
     * @param curRecord
     */
    protected abstract void _refreshDB(Uri uri, FileRecord curRecord);

    protected void _clearCache(FileRecord record) {
    }

    protected boolean needClearDB() {
        return false;
    }

    protected void _stopRefreshDb() {
    }

    public boolean isColumnExists(SQLiteDatabase db, String table, String column) {
        boolean ret = false;
        try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    String name = cursor.getString(cursor.getColumnIndex("name"));
                    if (column != null && column.equalsIgnoreCase(name)) {
                        ret = true;
                    }
                }
            }
        }
        return ret;
    }
}
